// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Copyright (C) 2016-2018, LomboTech Co.Ltd.
 * Authors:
 *	lomboswer <lomboswer@lombotech.com>
 *
 * This program is free software; you can redistribute  it and/or modify it
 * under  the terms of  the GNU General  Public License as published by the
 * Free Software Foundation;  either version 2 of the  License, or (at your
 * option) any later version.
 */

#include <drm/drmP.h>
#include <drm/drm_atomic_helper.h>
#include <drm/drm_crtc_helper.h>
#include <drm/drm_panel.h>
#include <linux/media-bus-format.h>

#include "lombo_drv.h"
#include "lombo_doss.h"
#include "lombo_tcon.h"
#include "lombo_vo_manage.h"

struct lombo_rgb {
	struct drm_connector connector;
	struct drm_encoder encoder;
	struct drm_device *drm;
	struct device *doss_top;
	uint32_t possible_crtcs;
	struct lombo_vo_host host;
	struct lombo_vo_dev_config vo;
	struct lombo_doss_clk *lvds_clk[2];

	struct drm_panel *panel;
	bool prepared;
	u32 cmd_align;
	unsigned long flag;
	enum lombo_vo_pixel_format format;
	vo_dev_if_t dev_if;
	u32 clk_dly;
};

static inline struct lombo_rgb *to_lombo_rgb_by_enc(
		struct drm_encoder *enc)
{
	return container_of(enc, struct lombo_rgb, encoder);
}

static inline struct lombo_rgb *to_lombo_rgb_by_con(
		struct drm_connector *con)
{
	return container_of(con, struct lombo_rgb, connector);
}

static inline struct lombo_rgb *to_lombo_rgb_by_host(
	struct lombo_vo_host *host)
{
	return container_of(host, struct lombo_rgb, host);
}

static int lombo_rgb_encoder_atomic_check(struct drm_encoder *enc,
	struct drm_crtc_state *crtc_state,
	struct drm_connector_state *conn_state)
{
	struct lombo_rgb *rgb = to_lombo_rgb_by_enc(enc);
	struct drm_panel *p = rgb->panel;
	struct drm_display_mode *mode = &crtc_state->mode;
	struct drm_display_mode *adjusted_mode = &crtc_state->adjusted_mode;
	struct drm_connector *conn = &rgb->connector;
	unsigned long flags = rgb->flag;

	if (p && p->funcs && p->funcs->get_mode_private)
		p->funcs->get_mode_private(p,
			mode, (void **)&(adjusted_mode->private));
	if (!adjusted_mode->private) {
		if (conn->connector_type == DRM_MODE_CONNECTOR_LVDS) {
			rgb->vo.dev_if = VO_DEV_LVDS;
			rgb->vo.lvds_if.is_ctrl_invert =
				(flags & LOMBO_VO_MODE_DATA_LSB_TO_MSB) ?
					true : false;
			rgb->vo.lvds_if.mode = (flags & LOMBO_VO_MODE_JEIDA) ?
				TCON_LVDS_JEIDA : TCON_LVDS_VESA;
			switch (rgb->format) {
			case LOMBO_VO_FMT_RGB888:
				rgb->vo.lvds_if.bpp = TCON_COLOR_24BPP;
				break;
			case LOMBO_VO_FMT_RGB666:
				rgb->vo.lvds_if.bpp = TCON_COLOR_18BPP;
				break;
			case LOMBO_VO_FMT_RGB565:
				rgb->vo.lvds_if.bpp = TCON_COLOR_16BPP;
				break;
			default:
				DRM_ERROR("invalid output format for lvds\n");
				break;
			}
		} else if (conn->connector_type == DRM_MODE_CONNECTOR_RGB) {
			rgb->vo.rgb_if.vsync_pol =
				(flags & LOMBO_VO_MODE_PVSYNC) ?
					TCON_IO_POS : TCON_IO_NEG;
			rgb->vo.rgb_if.hsync_pol =
				(flags & LOMBO_VO_MODE_PHSYNC) ?
					TCON_IO_POS : TCON_IO_NEG;
			rgb->vo.rgb_if.de_pol =
				(flags & LOMBO_VO_MODE_PDATA) ?
					TCON_IO_POS : TCON_IO_NEG;
			rgb->vo.rgb_if.clk_pol =
				(flags & LOMBO_VO_MODE_PCLOCK) ?
					TCON_IO_RISING : TCON_IO_FALLING;
			rgb->vo.rgb_if.is_clk_ddr = false;
		} else if (conn->connector_type == DRM_MODE_CONNECTOR_CPU) {
			if (flags & LOMBO_VO_MODE_HSYNC_CMD)
				rgb->vo.cpu_if.has_hsync_cmd = true;
			if (flags & LOMBO_VO_MODE_VSYNC_CMD)
				rgb->vo.cpu_if.has_vsync_cmd = true;
			if (flags & LOMBO_VO_MODE_WITH_TE)
				rgb->vo.has_te = true;
			if (flags & LOMBO_VO_MODE_POS_TE)
				rgb->vo.cpu_if.te_pol = TCON_IO_POS;
			rgb->vo.te_pol = rgb->vo.cpu_if.te_pol;
			rgb->vo.cpu_if.cmd_align = rgb->cmd_align;
		} else if (conn->connector_type == DRM_MODE_CONNECTOR_BT) {
			rgb->vo.bt_if.field_pol = TCON_IO_POS;
			rgb->vo.bt_if.vb_pol = TCON_IO_POS;
			rgb->vo.bt_if.hb_pol = TCON_IO_POS;
			rgb->vo.bt_if.field_map = TCON_FUNC_MAP_FIELD;
			rgb->vo.bt_if.vs_map = TCON_FUNC_MAP_VSYNC;
			rgb->vo.bt_if.hs_map = TCON_FUNC_MAP_HSYNC;
			rgb->vo.bt_if.de_map = TCON_FUNC_MAP_DE;
			rgb->vo.bt_if.ds_mode = TCON_BT_YC_DS_DIRECTLY;
			rgb->vo.bt_if.yc_senquence = TCON_BT_YC_SEQ_Y0CB0Y1CR0;
			rgb->vo.bt_if.is_clk_ddr = false;

			rgb->vo.frc_en = false;
			rgb->vo.csc_en = true;
			rgb->vo.has_te = false;
			rgb->vo.te_pol = TCON_IO_POS;
		} else
			DRM_ERROR("no support connector_type %d\n",
				conn->connector_type);

		rgb->vo.timing.is_interlace =
			mode->flags & DRM_MODE_FLAG_INTERLACE;

		if (rgb->vo.timing.is_interlace) {
			rgb->vo.timing.field1.vt = mode->vtotal - mode->vdisplay / 2;
			rgb->vo.timing.field1.vact = mode->vdisplay / 2;
			rgb->vo.timing.field1.vfp = mode->vsync_start - mode->vdisplay;
			rgb->vo.timing.field1.vsw = mode->vsync_end - mode->vsync_start;
			rgb->vo.timing.field2.vt = mode->vtotal - mode->vdisplay / 2 + 1;
			rgb->vo.timing.field2.vact = mode->vdisplay / 2;
			rgb->vo.timing.field2.vfp = mode->vsync_start - mode->vdisplay;
			rgb->vo.timing.field2.vsw = mode->vsync_end - mode->vsync_start;
			rgb->vo.timing.field2.vfp_float = 1;
			rgb->vo.timing.hline.ht = mode->htotal;
			rgb->vo.timing.hline.hact = mode->hdisplay;
			rgb->vo.timing.hline.hfp = mode->hsync_start - mode->hdisplay;
			rgb->vo.timing.hline.hsw = mode->hsync_end - mode->hsync_start;
		} else {
			rgb->vo.timing.field1.vt = mode->vtotal;
			rgb->vo.timing.field1.vact = mode->vdisplay;
			rgb->vo.timing.field1.vfp = mode->vsync_start - mode->vdisplay;
			rgb->vo.timing.field1.vsw = mode->vsync_end - mode->vsync_start;
			rgb->vo.timing.hline.ht = mode->htotal;
			rgb->vo.timing.hline.hact = mode->hdisplay;
			rgb->vo.timing.hline.hfp = mode->hsync_start - mode->hdisplay;
			rgb->vo.timing.hline.hsw = mode->hsync_end - mode->hsync_start;
		}
		rgb->vo.timing.dclk_freq = mode->clock * 1000;
		rgb->vo.timing.width = mode->hdisplay;
		rgb->vo.timing.height = mode->vdisplay;
		rgb->vo.format = (tcon_output_fmt_t)rgb->format;
		rgb->vo.dev_if = rgb->dev_if;
		rgb->vo.clk_dly = rgb->clk_dly;

		adjusted_mode->private = (int *)&rgb->vo;

	}

	return 0;
}

static void lombo_rgb_encoder_atomic_mode_set(
	struct drm_encoder *enc,
	struct drm_crtc_state *crtc_state,
	struct drm_connector_state *conn_state)
{
	DRM_DEBUG_ATOMIC("%s, %d\n", __func__, __LINE__);

	if (enc->encoder_type == DRM_MODE_ENCODER_LVDS) {
		struct lombo_rgb *rgb = to_lombo_rgb_by_enc(enc);
		struct drm_display_mode *mode = &crtc_state->adjusted_mode;
		struct lombo_vo_dev_config *vo_dev_cfg =
			(struct lombo_vo_dev_config *)(mode->private);

		if (!vo_dev_cfg) {
			DRM_ERROR("null vo_dev_cfg info\n");
			return;
		}
		rgb->vo.lvds_if.is_dual_link =
			vo_dev_cfg->lvds_if.is_dual_link;
		DRM_DEBUG_KMS("lvds%d with slave\n", rgb->host.host_id);
	}
}

static void lombo_rgb_set_lvds_clk(struct lombo_rgb *rgb, int enable, int sw)
{
	struct lombo_vo_dev_config *vo = &rgb->vo;
	struct lombo_doss_clk *lvds_clk = rgb->lvds_clk[0];
	struct lombo_doss_clk *slave_lvds_clk = rgb->lvds_clk[1];
	unsigned long best_rate, req_rate;
	uint64_t tmp;
	uint32_t rem;
	bool dual_link = rgb->vo.lvds_if.is_dual_link;

	if (!enable) {
		lombo_doss_clk_disable(lvds_clk);
		lombo_doss_path_set(rgb->doss_top, rgb->host.host_id,
				DOSS_CTRL_LVDS, 0);
		if (dual_link) {
			lombo_doss_clk_disable(slave_lvds_clk);
			lombo_doss_path_set(rgb->doss_top,
				slave_lvds_clk->path_id, DOSS_CTRL_LVDS, 1);
		}
		return;
	}

	if (sw) {
		lombo_doss_clk_enable(lvds_clk, 1);
		return;
	}
	lombo_doss_path_set(rgb->doss_top, lvds_clk->path_id,
				DOSS_CTRL_LVDS, 1);

	req_rate = (unsigned long)vo->timing.dclk_freq * 7;
	lombo_doss_clk_set_rate(lvds_clk, req_rate);
	lombo_doss_get_best_sclk_rate(lvds_clk,
		DOSS_CLK_LVDS, &best_rate);

	tmp = best_rate;
	rem = do_div(tmp, req_rate);
	if (rem > (uint32_t)(req_rate + 1) >> 1)
		tmp++;
	if (tmp == 0)
		tmp++;
	lvds_clk->div0 = (uint32_t)tmp;
	lvds_clk->div1 = 1;
	tmp = best_rate;
	do_div(tmp, lvds_clk->div0);
	lombo_doss_clk_enable(lvds_clk, 0);

	if (dual_link) {
		lombo_doss_path_set(rgb->doss_top, slave_lvds_clk->path_id,
				DOSS_CTRL_LVDS, 1);
		lombo_doss_clk_set_rate(slave_lvds_clk, req_rate);
		slave_lvds_clk->div0 = lvds_clk->div0;
		slave_lvds_clk->div1 = lvds_clk->div1;
		lombo_doss_clk_enable(slave_lvds_clk, 0);
	}
}

static void lombo_rgb_encoder_enable(struct drm_encoder *enc)
{
	struct lombo_rgb *rgb = to_lombo_rgb_by_enc(enc);

	DRM_DEBUG_ATOMIC("%s\n", __func__);

	if (lombo_disp_boot_sync(enc->crtc)) {
		struct drm_panel *p = rgb->panel;

		if (enc->encoder_type == DRM_MODE_ENCODER_LVDS)
			lombo_rgb_set_lvds_clk(rgb, 1, 1);

		if (p && p->funcs && p->funcs->boot_sync) {
			p->funcs->boot_sync(p, LOMBO_PANEL_BOOT_SYNC_PREPARE);
			p->funcs->boot_sync(p, LOMBO_PANEL_BOOT_SYNC_ENABLE);
		}
		return;
	}

	if (enc->encoder_type == DRM_MODE_ENCODER_LVDS) {
		bool dual_link = rgb->vo.lvds_if.is_dual_link;

		lombo_rgb_set_lvds_clk(rgb, 1, 0);
		lombo_doss_vref_config(rgb->doss_top,
				rgb->host.host_id, DOSS_CTRL_LVDS, 1);
		if (dual_link) {
			lombo_doss_vref_config(rgb->doss_top,
				rgb->host.host_id ? 0 : 1, DOSS_CTRL_LVDS, 1);
			lombo_doss_slave_config(rgb->doss_top,
				rgb->host.host_id ? 0 : 1, DOSS_CTRL_LVDS, 1);
		}
	}

	drm_panel_prepare(rgb->panel);
	drm_panel_enable(rgb->panel);
}

static void lombo_rgb_encoder_disable(struct drm_encoder *enc)
{
	struct lombo_rgb *rgb = to_lombo_rgb_by_enc(enc);

	DRM_DEBUG_ATOMIC("%s\n", __func__);

	drm_panel_disable(rgb->panel);
	drm_panel_unprepare(rgb->panel);

	if (enc->encoder_type == DRM_MODE_ENCODER_LVDS) {
		bool dual_link = rgb->vo.lvds_if.is_dual_link;

		lombo_doss_vref_config(rgb->doss_top,
				rgb->host.host_id, DOSS_CTRL_LVDS, 0);
		if (dual_link) {
			lombo_doss_vref_config(rgb->doss_top,
				rgb->host.host_id ? 0 : 1, DOSS_CTRL_LVDS, 0);
			lombo_doss_vref_config(rgb->doss_top,
				rgb->host.host_id ? 0 : 1, DOSS_CTRL_LVDS, 0);
		}
		lombo_rgb_set_lvds_clk(rgb, 0, 0);
	}
}

static const struct drm_encoder_helper_funcs lombo_rgb_enc_helper_funcs = {
	.atomic_check		= lombo_rgb_encoder_atomic_check,
	.atomic_mode_set	= lombo_rgb_encoder_atomic_mode_set,
	.enable			= lombo_rgb_encoder_enable,
	.disable		= lombo_rgb_encoder_disable,
};

static struct drm_encoder_funcs lombo_rgb_enc_funcs = {
	.destroy = drm_encoder_cleanup,
};

static int lombo_rgb_connector_get_modes(
	struct drm_connector *con)
{
	struct lombo_rgb *rgb = to_lombo_rgb_by_con(con);

	DRM_DEBUG_ATOMIC("%s\n", __func__);
	return drm_panel_get_modes(rgb->panel);
}

static struct drm_connector_helper_funcs lombo_rgb_con_helper_funcs = {
	.get_modes = lombo_rgb_connector_get_modes,
};

static enum drm_connector_status lombo_rgb_connector_detect(
	struct drm_connector *con, bool force)
{
	DRM_DEBUG_ATOMIC("connector status[%d]\n", con->status);

	return con->status;
}

static struct drm_connector_funcs lombo_rgb_con_funcs = {
	.detect			= lombo_rgb_connector_detect,
	.reset			= drm_atomic_helper_connector_reset,
	.fill_modes		= drm_helper_probe_single_connector_modes,
	.destroy		= drm_connector_cleanup,
	.atomic_duplicate_state	= drm_atomic_helper_connector_duplicate_state,
	.atomic_destroy_state	= drm_atomic_helper_connector_destroy_state,
};

static int lombo_rgb_vo_host_attach(struct lombo_vo_host *host,
	struct lombo_vo_device *vo)
{
	struct drm_encoder *encoder;
	struct drm_panel *panel;
	struct lombo_rgb *rgb = to_lombo_rgb_by_host(host);
	int encoder_type = DRM_MODE_ENCODER_NONE;
	int ret;

	DRM_DEBUG_DRIVER("%d\n", __LINE__);

	rgb->vo.dev_if = (vo_dev_if_t)vo->interface;
	rgb->cmd_align = vo->cmd_align;
	rgb->flag = vo->mode_flags;
	rgb->format = vo->format;
	rgb->dev_if = (vo_dev_if_t)vo->interface;
	rgb->clk_dly = vo->clk_dly;

	panel = of_drm_find_panel(vo->dev.of_node);
	if (IS_ERR_OR_NULL(panel)) {
		DRM_INFO("no available panel\n");
		rgb->connector.status = connector_status_disconnected;
		return -ENODEV;
	}
	rgb->panel = panel;

	switch (rgb->connector.connector_type) {
	case DRM_MODE_CONNECTOR_CPU:
		encoder_type = DRM_MODE_ENCODER_CPU;
		break;
	case DRM_MODE_CONNECTOR_RGB:
		encoder_type = DRM_MODE_ENCODER_RGB;
		break;
	case DRM_MODE_CONNECTOR_LVDS:
		encoder_type = DRM_MODE_ENCODER_LVDS;
		break;
	case DRM_MODE_CONNECTOR_TV:
		encoder_type = DRM_MODE_ENCODER_TVDAC;
		break;
	case DRM_MODE_CONNECTOR_BT:
		encoder_type = DRM_MODE_ENCODER_BT;
		break;
	default:
		DRM_ERROR("unsupported vo host type\n");
		return -ENODEV;
	}

	encoder = &rgb->encoder;
	drm_encoder_helper_add(encoder, &lombo_rgb_enc_helper_funcs);
	ret = drm_encoder_init(rgb->drm, encoder,
			&lombo_rgb_enc_funcs,
			encoder_type, NULL);
	if (ret) {
		DRM_ERROR("lombo vo encoder init fail\n");
		return ret;
	}
	encoder->possible_crtcs = rgb->possible_crtcs;

	drm_connector_attach_encoder(&rgb->connector, encoder);
	drm_panel_attach(panel, &rgb->connector);

	if (rgb->drm->mode_config.poll_enabled)
		drm_kms_helper_hotplug_event(rgb->drm);

	return 0;
}

static int lombo_rgb_vo_host_detach(struct lombo_vo_host *host,
	struct lombo_vo_device *vo)
{
	struct lombo_rgb *rgb = to_lombo_rgb_by_host(host);

	DRM_DEBUG_DRIVER("%d\n", __LINE__);

	if (rgb->panel) {
		drm_panel_detach(rgb->panel);
		rgb->connector.status = connector_status_disconnected;
	}

	if (rgb->drm->mode_config.poll_enabled)
		drm_kms_helper_hotplug_event(rgb->drm);

	return 0;
}

static ssize_t lombo_rgb_vo_host_transfer(
	struct lombo_vo_host *host, const struct lombo_vo_msg *msg)
{
	u32 *p = msg->tx_buf;

	switch (msg->type) {
	case LOMBO_CPU_IF_MSG_WR_REG: {
		csp_tcon_cpu_write_reg(host->host_id, *p,
				p + 1, (msg->tx_len - 1));
		break;
		}
	case LOMBO_CPU_IF_MSG_WR_IDX: {
		csp_tcon_cpu_write_reg_index(host->host_id, *p);
		break;
		}
	default:
		DRM_ERROR("msg->type=0x%x\n", msg->type);
		break;
	}

	return 0;
}

struct lombo_vo_host_ops lombo_rgb_vo_host_ops = {
	.attach = lombo_rgb_vo_host_attach,
	.detach = lombo_rgb_vo_host_detach,
	.transfer = lombo_rgb_vo_host_transfer,
};

struct lombo_rgb *lombo_rgb_init(struct drm_device *drm, struct device *dev,
	struct device *doss_top, uint32_t host_id, uint32_t possible_crtcs)
{
	struct lombo_rgb *rgb;
	struct device_node *child, *np = dev->of_node;
	u32 reg = 0;
	int ret, connector_type = DRM_MODE_CONNECTOR_Unknown;

	child = of_get_next_available_child(np, NULL);
	if (!child) {
		DRM_DEBUG_KMS("no available lombo vo device node\n");
		return NULL;
	}

	rgb = kzalloc(sizeof(*rgb), GFP_KERNEL);
	if (!rgb)
		return NULL;

	ret = of_property_read_u32(child, "reg", &reg);
	if (ret)
		dev_err(dev, "device node %pOF has no valid reg property: %d\n",
			child, ret);

	switch (reg) {
	case 0:
		connector_type = DRM_MODE_CONNECTOR_LVDS;
		break;
	case 1:
		connector_type = DRM_MODE_CONNECTOR_RGB;
		break;
	case 2:
		connector_type = DRM_MODE_CONNECTOR_CPU;
		break;
	case 3:
		connector_type = DRM_MODE_CONNECTOR_TV;
		break;
	case 4:
		connector_type = DRM_MODE_CONNECTOR_BT;
		break;
	default:
		DRM_INFO("invalid reg[%d]\n, type lvds deaault\n", reg);
		connector_type = DRM_MODE_CONNECTOR_LVDS;
	}

	if (connector_type == DRM_MODE_CONNECTOR_LVDS) {
		rgb->lvds_clk[0] = lombo_doss_get_clk(
				dev, doss_top, host_id, DOSS_CLK_LVDS);
		rgb->lvds_clk[1] = lombo_doss_get_clk(
				dev, doss_top, host_id ? 0 : 1, DOSS_CLK_LVDS);
		if (!rgb->lvds_clk[0] || !rgb->lvds_clk[1]) {
			DRM_ERROR("lombo_doss_get_clk for lvds fail\n");
			goto err_out;
		}
		rgb->doss_top = doss_top;
	}

	rgb->possible_crtcs = possible_crtcs;
	rgb->drm = drm;
	rgb->host.dev = dev;
	rgb->host.ops = &lombo_rgb_vo_host_ops;
	rgb->host.host_id = host_id;

	/* init a default connector for setup fbdev first */
	rgb->connector.dpms = DRM_MODE_DPMS_OFF;
	drm_connector_helper_add(&rgb->connector, &lombo_rgb_con_helper_funcs);
	ret = drm_connector_init(rgb->drm, &rgb->connector,
			&lombo_rgb_con_funcs, connector_type);
	if (ret) {
		DRM_ERROR("lombo vo connector init fail\n");
		goto err_out;
	}
	rgb->connector.interlace_allowed = true;
	rgb->connector.status = connector_status_connected;
	rgb->connector.polled = DRM_CONNECTOR_POLL_HPD;

	lombo_vo_host_register(&rgb->host);

	return rgb;

err_out:
	if (connector_type == DRM_MODE_CONNECTOR_LVDS) {
		if (rgb->lvds_clk[0])
			lombo_doss_put_clk(&rgb->lvds_clk[0]);
		if (rgb->lvds_clk[1])
			lombo_doss_put_clk(&rgb->lvds_clk[1]);
	}
	kfree(rgb);
	return NULL;
}

int lombo_rgb_deinit(struct lombo_rgb *rgb)
{
	if (!rgb)
		return -EINVAL;

	lombo_vo_host_unregister(&rgb->host);
	kfree(rgb);

	return 0;
}
